package com.gkzf.ai.module.infra.framework.file.core.client.s3;

import cn.hutool.core.io.IoUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.HttpUtil;
import com.amazonaws.HttpMethod;
import com.amazonaws.auth.AWSStaticCredentialsProvider;
import com.amazonaws.auth.BasicAWSCredentials;
import com.amazonaws.client.builder.AwsClientBuilder;
import com.amazonaws.services.s3.AmazonS3Client;
import com.amazonaws.services.s3.AmazonS3ClientBuilder;
import com.amazonaws.services.s3.model.ObjectMetadata;
import com.amazonaws.services.s3.model.S3Object;
import com.gkzf.ai.framework.common.exception.ServerException;
import com.gkzf.ai.module.infra.framework.file.core.client.AbstractFileClient;
import com.volcengine.tos.TOSV2;
import com.volcengine.tos.TOSV2ClientBuilder;
import com.volcengine.tos.model.object.PutObjectFromFileInput;
import lombok.extern.slf4j.Slf4j;
import org.springframework.retry.policy.SimpleRetryPolicy;
import org.springframework.retry.support.RetryTemplate;

import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.util.Collections;
import java.util.Date;
import java.util.concurrent.TimeUnit;

/**
 * 基于 S3 协议的文件客户端，实现 MinIO、阿里云、腾讯云、七牛云、华为云等云服务
 * <p>
 * S3 协议的客户端，采用亚马逊提供的 software.amazon.awssdk.s3 库
 *
 * @author 国科智飞
 */
@Slf4j
public class S3FileClient extends AbstractFileClient<S3FileClientConfig> {

    private AmazonS3Client client;

    public S3FileClient(Long id, S3FileClientConfig config) {
        super(id, config);
    }

    @Override
    protected void doInit() {
        // 补全 domain
        if (StrUtil.isEmpty(config.getDomain())) {
            config.setDomain(buildDomain());
        }
        // 初始化客户端
        client = (AmazonS3Client)AmazonS3ClientBuilder.standard()
                .withCredentials(buildCredentials())
                .withEndpointConfiguration(buildEndpointConfiguration())
                .build();
    }

    /**
     * 基于 config 秘钥，构建 S3 客户端的认证信息
     *
     * @return S3 客户端的认证信息
     */
    private AWSStaticCredentialsProvider buildCredentials() {
        return new AWSStaticCredentialsProvider(
                new BasicAWSCredentials(config.getAccessKey(), config.getAccessSecret()));
    }

    /**
     * 构建 S3 客户端的 Endpoint 配置，包括 region、endpoint
     *
     * @return  S3 客户端的 EndpointConfiguration 配置
     */
    private AwsClientBuilder.EndpointConfiguration buildEndpointConfiguration() {
        return new AwsClientBuilder.EndpointConfiguration(config.getEndpoint(),
                null); // 无需设置 region
    }

    /**
     * 基于 bucket + endpoint 构建访问的 Domain 地址
     *
     * @return Domain 地址
     */
    private String buildDomain() {
        // 如果已经是 http 或者 https，则不进行拼接.主要适配 MinIO
        if (HttpUtil.isHttp(config.getEndpoint()) || HttpUtil.isHttps(config.getEndpoint())) {
            return StrUtil.format("{}/{}", config.getEndpoint(), config.getBucket());
        }
        // 阿里云、腾讯云、华为云都适合。七牛云比较特殊，必须有自定义域名
        return StrUtil.format("https://{}.{}", config.getBucket(), config.getEndpoint());
    }

//    @Override
//    public String upload(byte[] content, String path, String type) throws Exception {
//        // 元数据，主要用于设置文件类型
//        ObjectMetadata objectMetadata = new ObjectMetadata();
//        objectMetadata.setContentType(type);
//        objectMetadata.setContentLength(content.length); // 如果不设置，会有 “ No content length specified for stream data” 警告日志
//        // 执行上传
//        client.putObject(config.getBucket(),
//                path, // 相对路径
//                new ByteArrayInputStream(content), // 文件内容
//                objectMetadata);
//
//        // 拼接返回路径
//        return config.getDomain() + "/" + path;
//    }

    /**
     * 将字节数组写入指定路径的文件，并返回对应的File对象。
     * 如果文件已存在，默认会覆盖其内容。
     * 如果父目录不存在，会自动创建。
     *
     * @param content 要写入文件的字节内容，不能为 null。
     * @param filePath 要创建的文件的完整路径，例如: "/data/reports/my_document.pdf" 或 "temp/image.png"
     * @return 表示新创建文件的 File 对象。
     * @throws IOException 如果写入文件过程中发生IO错误。
     * @throws IllegalArgumentException 如果输入的 content 为 null。
     */
    public static File bytesToFile(byte[] content, String filePath) throws IOException {
        if (content == null) {
            throw new IllegalArgumentException("Input byte array 'content' cannot be null");
        }
        if (filePath == null || filePath.trim().isEmpty()) {
            throw new IllegalArgumentException("Input file path 'filePath' cannot be null or empty");
        }

        File file = new File(filePath);

        // 确保父目录存在
        File parentDir = file.getParentFile();
        if (parentDir != null && !parentDir.exists()) {
            // mkdirs() 方法会创建所有必要的父目录
            if (!parentDir.mkdirs()) {
                throw new IOException("Failed to create parent directories for path: " + parentDir.getAbsolutePath());
            }
        }

        // 使用 try-with-resources 确保流自动关闭
        try (FileOutputStream fos = new FileOutputStream(file);
             BufferedOutputStream bos = new BufferedOutputStream(fos)) { // 提高写入性能
            bos.write(content);
            // bos.flush(); // flush 通常在 close() 时自动完成，非必要
        } catch (IOException e) {
            // 如果写入文件失败，可以考虑删除可能部分创建的文件
            // file.delete(); // 可选：尝试清理
            throw new IOException("Failed to write content to file: " + filePath, e);
        } // try-with-resources 会在这里自动关闭 bos 和 fos

        // 返回表示该路径的 File 对象
        return file;
    }


    @Override
    public String upload(byte[] content, String path, String type) throws Exception {
        // 配置RetryTemplate 提供了编程式重试的 API
        RetryTemplate retryTemplate = new RetryTemplate();
        // 4 设置重试策略 最大重试 3 次，只重试 RuntimeException
        SimpleRetryPolicy retryPolicy = new SimpleRetryPolicy(3, Collections.singletonMap(ServerException.class, true));
        retryTemplate.setRetryPolicy(retryPolicy);
         retryTemplate.execute(context -> {
            // 元数据，主要用于设置文件类型
            ObjectMetadata objectMetadata = new ObjectMetadata();
            objectMetadata.setContentType(type);
            objectMetadata.setContentLength(content.length); // 如果不设置，会有 “ No content length specified for stream data” 警告日志
            TOSV2 tos = new TOSV2ClientBuilder().build("cn-beijing", config.getEndpoint(), config.getAccessKey(), config.getAccessSecret());
            File file = null;
            try {
                // 执行上传
                file = bytesToFile(content, path);
                log.debug("创建临时文件: {}", file.getAbsolutePath());
                PutObjectFromFileInput putObjectInput = new PutObjectFromFileInput().setBucket(config.getBucket()).setKey(path).setFile(file);
                return tos.putObjectFromFile(putObjectInput);
            } catch (Exception e) {
                log.info("上传文件失败，重试次数: {}, 异常描述：{}", context.getRetryCount(),e.getMessage());
                Thread.sleep(3000);
                throw new ServerException();
            } finally {
                // 确保临时文件被删除
                if (file != null && file.exists()) {
                    try {
                        Files.delete(file.toPath());
                        log.info("删除临时文件: {}", file.getAbsolutePath());
                    } catch (Exception e) {
                        // 删除临时文件失败，记录警告，但不影响上传结果
                        log.warn("删除临时文件失败。处理步骤: {}", file.getAbsolutePath(), e);
                    }
                }
            }
        });
        // 拼接返回路径
        return config.getDomain() + "/" + path;
    }

    @Override
    public void delete(String path) throws Exception {
        client.deleteObject(config.getBucket(), path);
    }

    @Override
    public byte[] getContent(String path) throws Exception {
        S3Object tempS3Object = client.getObject(config.getBucket(), path);
        return IoUtil.readBytes(tempS3Object.getObjectContent());
    }

    @Override
    public FilePresignedUrlRespDTO getPresignedObjectUrl(String path) throws Exception {
        // 设定过期时间为 10 分钟。取值范围：1 秒 ~ 7 天
        Date expiration = new Date(System.currentTimeMillis() + TimeUnit.MINUTES.toMillis(10));
        // 生成上传 URL
        String uploadUrl = String.valueOf(client.generatePresignedUrl(config.getBucket(), path, expiration , HttpMethod.PUT));
        return new FilePresignedUrlRespDTO(uploadUrl, config.getDomain() + "/" + path);
    }

}
